# [十] Spring AOP - ApectJ解析

导读

Spring AOPApectJ 的目的一致,都是为了统一处理横切业务,但与AspectJ不同的是,Spring AOP 并不尝试提供完整的AOP功能(即使它完全可以实现),Spring AOP更注重的是与Spring IOC容器的结合,并结合该优势来解决横切业务的问题,因此在AOP的功能完善方面,相对来说AspectJ具有更大的优势,同时,Spring注意到AspectJ在AOP的实现方式上依赖于特殊编译器(ajc编译器),因此Spring很机智回避了这点,转向采用动态代理技术的实现原理来构建Spring AOP的内部机制(动态织入),这是与AspectJ(静态织入)最根本的区别。

Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

# 开启Aspect静态代理

项目中如果某个类上使用了@Aspect注解,来使用面向切面编程处理业务,那么前提一定要先通知Spring 开启支持aspectj代理的配置,开启方式有两种:

  • 基于XMl配置:<aop:aspectj-autoproxy />
  • 基于注解配置:@EnableAspectJAutoProxy

# @EnableAspectJAutoProxy

测试类

@Service
/*
*  TODO: 
*  开启注解AOP
* */
@EnableAspectJAutoProxy(proxyTargetClass = false,exposeProxy = true)
public class EnableAspectJAutoProxyBean {

}

@EnableAspectJAutoProxy解读

  • 1、首先Srping容器初始化会通过@ComponentScan扫描到@Service
  • 2、接着会扫描@EnableAspectJAutoProxy注解中的@Import注解中的类。
  • 3、扫描完后会分别把EnableAspectJAutoProxyAspectJAutoProxyRegistrar注册到BeanDefinition对象中。
  • 4、继续循环AspectJAutoProxyRegistrar类中是否有@Component@ComponentScan@Import等注解,如果有继续注册到BeanDefinition对象中。

@EnableAspectJAutoProxy 注解源码

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
	/**
	 * true:
	 * 1、目标对象实现了接口 – 使用CGLIB代理机制
	 * 2、目标对象没有接口(只有实现类) – 使用CGLIB代理机制
	 *
	 * false:
	 * 1、目标对象实现了接口 – 使用JDK动态代理机制(代理所有实现了的接口)
	 * 2、目标对象没有接口(只有实现类) – 使用CGLIB代理机制
	 *
	 */
	boolean proxyTargetClass() default false;
	/**
	 *  是否由Spring AOP 暴露代理(代理对象用ThreadLocal存起来)
	 */
	boolean exposeProxy() default false;

}

@Import(AspectJAutoProxyRegistrar.class) 源码

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

	@Override
	public void registerBeanDefinitions(
			AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//注解AOP入口类,注册一个叫"org.springframework.aop.config.internalAutoProxyCreator"
//的自动代理bean名字,覆盖AnnotationAwareAspectJAutoProxyCreator类,
//并注册到BeanDefinition中。
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
		// 拿到EnableAspectJAutoProxy注解类中的属性值
		AnnotationAttributes enableAspectJAutoProxy =
				AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
		// 如果有属性值
		if (enableAspectJAutoProxy != null) {
			// 如果proxyTargetClass属性值为true
			if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
				// 强制使用代理类进行注册
				AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
			}
			//是否需要把代理对象暴露出来,简单来说是否需要把代理对象用ThreadLocal存起来,如果是true就是需要
			if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
				// 强制使用代理类进行注册
				AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
			}
		}
	}

}

问题

应用的业务场景:当一个类中A方法调用B方法执行事务操作时,B中不会有事务,因为a中调用b属于内部调用,没有通过代理,所以不会有事务产生。如何解决?

参考答案

首先设置expose-proxy()属性为true的时候,意味着将代理对象暴露出来,实际上调用了AopContext.setCurrentProxy(proxy)方法保存当前线程关联的AOP代理对象 到ThreadLocal中。再通过AopContext.currentProxy()获取当前代理,然后通过代理调用B()方法。即 (当前类)AopContext.currentProxy()).B()

AopContext 源码

public final class AopContext {
	/**
	 *  保存了当前线程关联的 AOP代理对象
	 */
	private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");
	private AopContext() {
	}
	/**
	 * TODO: 返回当前的代理对象
	 * 此方法使用的前提是通过AOP的方式激活,并且已经被AOP暴露
	 */
	public static Object currentProxy() throws IllegalStateException {
		Object proxy = currentProxy.get();
		if (proxy == null) {
			throw new IllegalStateException(
					"Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.");
		}
		return proxy;
	}
	@Nullable
	/**
	 * TODO: 保存代理对象到ThreadLocal
	 */
	static Object setCurrentProxy(@Nullable Object proxy) {
		Object old = currentProxy.get();
		if (proxy != null) {
			currentProxy.set(proxy);
		}
		else {
			currentProxy.remove();
		}
		return old;
	}

}

扩展阅读

判断一个Bean是否是AOP代理对象可以使用如下三种方法:

  • AopUtils.isAopProxy(bean) : 是否是代理对象;
  • AopUtils.isCglibProxy(bean): 是否是CGLIB方式的代理对象;
  • AopUtils.isJdkDynamicProxy(bean) : 是否是JDK动态代理方式的代理对象;

# 附:切面中的常用术语

  • 1)连接点(Joinpoint) 程序执行的某个特定位置:如类开始初始化前、类初始化后、类某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点,这些点中的特定点就称为连接点。Spring仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入增强。连接点由两个信息确定:第一是用方法表示的程序执行点;第二是用相对点表示的方位。

  • 2)切点(Pointcut) 每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。AOP通过“切点”定位特定的连接点。连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。在Spring中,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件,Spring AOP的规则解析引擎负责切点所设定的查询条件,找到对应的连接点。其实确切地说,不能称之为查询连接点,因为连接点是方法执行前、执行后等包括方位信息的具体程序执行点,而切点只定位到某个方法上,所以如果希望定位到具体连接点上,还需要提供方位信息。

  • 3)增强(Advice) 增强是织入到目标类连接点上的一段程序代码,在Spring中,增强除用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。结合执行点方位信息和切点信息,我们就可以找到特定的连接点。

  • 4)目标对象(Target) 增强逻辑的织入目标类。如果没有AOP,目标业务类需要自己实现所有逻辑,而在AOP的帮助下,目标业务类只实现那些非横切逻辑的程序逻辑,而性能监视和事务管理等这些横切逻辑则可以使用AOP动态织入到特定的连接点上。

  • 5)引介(Introduction) 引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。

  • 6)织入(Weaving) 织入是将增强添加对目标类具体连接点上的过程。AOP像一台织布机,将目标类、增强或引介通过AOP这台织布机天衣无缝地编织到一起。根据不同的实现技术,AOP有三种织入的方式:

    • a、编译期织入,这要求使用特殊的Java编译器。
    • b、类装载期织入,这要求使用特殊的类装载器。
    • c、动态代理织入,在运行期为目标类添加增强生成子类的方式。